在開發專案時,經常會遇到跨專案共用邏輯或工具的情況,例如常用的 logger 或驗證工具。如果每次都從一個專案複製貼上到另一個專案,難免會出現版本不同步或維護困難的問題。為了解決這類問題,我們可以將這些共用邏輯獨立成為 npm 套件,以便在多個專案中重複使用。
以我們之前實作的前後端範例為例,後端已經實作了驗證 Task 資料的 schema。如果我們希望在前端發送請求前,也對使用者的輸入進行驗證,有許多解決方案,其中一個做法是將這些驗證邏輯封裝成 npm 套件,並且支援 CommonJS (CJS) 和 ECMAScript 模組 (ESM) 兩種格式,以確保前後端都能使用相同的工具。
首先初始專案起手式
npm init -y
接著,安裝我們這次開發目標所需要的依賴套件。
npm i -D @types/node typescript rimraf
npm i --save-peer zod
在這裡,我們安裝了 rimraf
作為開發依賴,用來在不同平台上統一刪除檔案或資料夾。此外,我們還將 zod
加入 peerDependencies,這表示我們預期使用這個套件的開發者自己會安裝 zod
,並直接使用他們項目中的 zod
版本。
這樣的設置非常常見,例如當我們開發與 React
相關的套件時,我們也會將 React
安裝到 peerDependencies,讓使用者能自行管理 React
版本,而不會與我們的套件發生衝突。
因為執行 TypeScript 編譯時,一次只能產生一個輸出。要產生 CommonJS 和 ESM 程式碼,就會需要設定針對這兩個的設定檔。
首先,我們先配置共用的基本配置:
{
"compilerOptions": {
"lib": ["ESNext"],
"declaration": true,
"declarationDir": "./dist/types",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"baseUrl": ".",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}
該檔案指定了程式碼的位置、要排除的資料夾以及類型定義的輸出目錄等。
針對 CommonJS,我們將輸出位置配置如下:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs",
"target": "ES2020"
}
}
針對 ESM,我們將輸出位置配置如下:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "ES6",
"outDir": "./dist/esm",
"target": "ES2016"
}
}
設定完 TypeScript 後,我們來配置 package.json
。
name
:用來定義我們套件產生出來的名稱main
:我們 Node.js 主要執行的入口點types
:我們 Node.js 主要定義型別的檔案files
:選擇我們打包好的套件根資料夾
{
"name": "validator",
"version": "1.0.0",
"description": "",
"main": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts",
"files": [
"dist"
],
// ...
}
由於我們要同時支援 CommonJS 和 ESM,所以需要設定模組的匯出方式:
{
// ...
"exports": {
".": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts"
}
},
// ...
}
這裡我們為 ESM 模組使用 .mjs
副檔名,稍後將會撰寫腳本來替換編譯後的 .js
改為 .mjs
。
接著,我們撰寫打包與建置的 scripts。特別說明一下,npm pack
可以將專案壓縮成 npm 套件,而 prepack
會在執行 pack
之前自動執行。
{
// ...
"scripts": {
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json && npm run rename:esm",
"build": "npm run build:cjs && npm run build:esm",
"clean": "rimraf dist",
"rename:esm": "/bin/zsh ./scripts/fix-mjs.sh",
"prepack": "npm run clean && npm run build"
},
}
新增一個檔案 fix-mjs.sh
並撰寫替換 .js
到 .mjs
的腳本
for file in ./dist/esm/*.js; do
echo "Updating $file contents..."
sed -i '' "s/\.js'/\.mjs'/g" "$file"
echo "Renaming $file to ${file%.js}.mjs..."
mv "$file" "${file%.js}.mjs"
done
這邊就把原本寫在後端的驗證 schema 複製過來
import { z } from 'zod';
export const inputTaskSchema = z.object({
title: z.string().min(1, '請輸入任務標題'),
description: z.string().optional(),
status: z.enum(['new', 'active', 'completed']).default('new'),
storyPoint: z.number().optional(),
});
完成後,執行打包指令
npm pack
這時會看到專案內生成出一個 validator-1.0.0.tgz
,結構上就是 <package.name>-<package.version>.tgz
接著,到前後端專案中,安裝本地套件
npm i ../package/validator-1.0.0.tgz
這樣,前後端都可以使用同一套驗證邏輯了。
通過這個文章,我們學習了如何:
這個方法允許我們創建可在前端和後端共用的模組,提高代碼重用性和一致性。
本篇程式碼變更可以看此 PR